﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Threading;
using Lextm.SharpSnmpLib;
using Lextm.SharpSnmpLib.Messaging;
using System.Net.NetworkInformation;

namespace devicesDll
{
    public delegate void AsyncIdentifyDeviceFinishedEventHandler(object sender, IdentifyDeviceEventArgs e);
    public delegate void AsyncIdentifyDevicesFinishedEventHandler(object sender, IdentifyDevicesEventArgs e);
    public delegate void AsyncRecheckDeviceFinishedEventHandler(object sender, EventArgs e);
    public delegate void AsyncRecheckDevicesFinishedEventHandler(object sender, EventArgs e);

    public class IdentifyDevices
    {

        #region Variables
        Thread[] activeThreads;
        IdentifyThread[] identifyThreads;
        RecheckThread[] recheckThreads;
        List<DeviceSignature> deviceInformation;
        List<IPCollection> ips;
        int numberOfIPs = 0;
        int devicesQueried = 0;
        List<DeviceSignature> devices = new List<DeviceSignature>();
        bool queriesRunning = false;
        private object lockObject = new object();
        #endregion
        
        #region Events
        public event AsyncIdentifyDevicesFinishedEventHandler IdentifyDevicesFinished;
        public event AsyncRecheckDevicesFinishedEventHandler RecheckDevicesFinished;
        #endregion

        #region Methods
        /// <summary>
        /// Starts an asynchronous identify-request of all given Device Information
        /// </summary>
        /// <param name="deviceInformation">The List of given Device Information</param>
        /// <param name="maxThreads">number of maximum parallel working Threads</param>
        /// <param name="snmpPort">Port of the SNMP Query</param>
        /// <param name="timeout">Timeout of one Query</param>
        public void StartAsyncIdentify(List<DeviceSignature> deviceInformation,int maxThreads,int snmpPort, int timeout)
        {
            if (queriesRunning)
                throw new ApplicationException("Identify Queries already running");

            #region initialize

            if(deviceInformation.Count == 0 && IdentifyDevicesFinished != null)
                IdentifyDevicesFinished(this, new IdentifyDevicesEventArgs(new DeviceSignature[0]));

            queriesRunning = true;
            devicesQueried = 0; //reset variable
            activeThreads = new Thread[maxThreads]; //initialize array
            identifyThreads = new IdentifyThread[maxThreads]; //initialize array
            for(int i = 0;i<identifyThreads.Length;i++) //initialize each object in identify Threads
            {
                identifyThreads[i] = new IdentifyThread(snmpPort, timeout); //create object
                identifyThreads[i].IdentifySingleDeviceFinished+=new AsyncIdentifyDeviceFinishedEventHandler(IdentifyDevices_IdentifySingleDeviceFinished); //set finished event handler
            }
            numberOfIPs = deviceInformation.Count; //set maximum of devices to query
            #endregion

            int upperBorder; //calculate number of threads to start

            if (deviceInformation.Count <= maxThreads) //if more capacity is availiable than is needed
                upperBorder = deviceInformation.Count; //start all
            else
                upperBorder = maxThreads; //start as many as possible

            this.deviceInformation = deviceInformation; //save list
            DeviceSignature[] tempInf = deviceInformation.GetRange(0, upperBorder).ToArray();
            this.deviceInformation.RemoveRange(0, upperBorder); //remove first elements (they will be queried immediatly
            for (int i = 0; i < upperBorder; i++)
            {
                identifyThreads[i].SetDeviceSignature(tempInf[i]); //give Information
                activeThreads[i] = new Thread(identifyThreads[i].IdentifyMethod); //initialize new thread
                activeThreads[i].Start(); //start thread
            }
        }

        public void StartAsyncIPCheck(List<IPCollection> ips, int maxThreads, int timeout)
        {
            if (queriesRunning)
                throw new ApplicationException("Identify Queries already running");

            #region initialize

            if ((ips == null || ips.Count == 0) && RecheckDevicesFinished != null)
            {
                this.RecheckDevicesFinished(this, EventArgs.Empty);
                return;
            }

            queriesRunning = true;
            devicesQueried = 0; //reset variable
            activeThreads = new Thread[maxThreads]; //initialize array
            recheckThreads = new RecheckThread[maxThreads]; //initialize array
            for (int i = 0; i < recheckThreads.Length; i++) //initialize each object in identify Threads
            {
                recheckThreads[i] = new RecheckThread(timeout); //create object
                recheckThreads[i].SingleDeviceFinished += new AsyncRecheckDeviceFinishedEventHandler(IdentifyDevices_SingleDeviceFinished); //set finished event handler
            }
            numberOfIPs = ips.Count; //set maximum of devices to query
            #endregion

            int upperBorder; //calculate number of threads to start

            if (ips.Count <= maxThreads) //if more capacity is availiable than is needed
                upperBorder = numberOfIPs; //start all
            else
                upperBorder = maxThreads; //start as many as possible

            this.ips = ips; //save list
            IPCollection[] tempInf = ips.GetRange(0, upperBorder).ToArray();
            this.ips.RemoveRange(0, upperBorder); //remove first elements (they will be queried immediatly
            for (int i = 0; i < upperBorder; i++)
            {
                recheckThreads[i].SetIps(tempInf[i]); //give Information
                activeThreads[i] = new Thread(recheckThreads[i].CheckMethod); //initialize new thread
                activeThreads[i].Start(); //start thread
            }
        }

        private void IdentifyDevices_IdentifySingleDeviceFinished(object sender, IdentifyDeviceEventArgs e)
        {

            lock (lockObject)
            {
                this.devices.Add(e.Signature);
                devicesQueried++; //count finished queries
                if (devicesQueried >= numberOfIPs) //if all queries are finished
                {
                    queriesRunning = false; //reset blocking bool
                    if (this.IdentifyDevicesFinished != null) //if event is hooked up
                        this.IdentifyDevicesFinished(this, new IdentifyDevicesEventArgs(devices.ToArray())); //throw event
                
                }

                if (this.deviceInformation.Count == 0)
                {
                    ((IdentifyThread)sender).StopThread();
                }
                else
                {
                    DeviceSignature currentsig;
                    currentsig = deviceInformation[0];
                    deviceInformation.RemoveAt(0);

                    ((IdentifyThread)sender).SetDeviceSignature(currentsig);
                }
            }
        }

        private void IdentifyDevices_SingleDeviceFinished(object sender, EventArgs e)
        {
            lock (lockObject)
            {
                devicesQueried++; //count finished queries
                if (devicesQueried >= numberOfIPs) //if all queries are finished
                {
                    queriesRunning = false; //reset blocking bool
                    if (this.RecheckDevicesFinished != null) //if event is hooked up
                        this.RecheckDevicesFinished(this, EventArgs.Empty); //throw event

                }

                if (this.ips.Count == 0)
                {
                    ((RecheckThread)sender).StopThread();
                }
                else
                {
                    IPCollection currentIPs;
                    currentIPs = ips[0];
                    ips.RemoveAt(0);

                    ((RecheckThread)sender).SetIps(currentIPs);
                }
            }
        }
        
        /// <summary>
        /// Converts an array of Ip Adresses as strings to array of IPAddress objects
        /// </summary>
        /// <param name="ips">Ip Adresses as string</param>
        /// <returns>Ip Address Array of given strings</returns>
        public static IPAddress[] ConvertStringToIP(string[] ips)
        {
            List<IPAddress> convertedIps = new List<IPAddress>(ips.Length); //create an array with specific length
            for (int i = 0; i < ips.Length; i++) //for every given IP Address
            {
                try
                {
                    convertedIps.Add(IPAddress.Parse(ips[i]));   //Parse
                }
                catch { }
            }
            return convertedIps.ToArray();
        }
        #endregion

    }

    public class IdentifyThread
    {

        #region Constants
        const int timeoutDivisor = 5;
        const int numberOfPings = 2;
        #endregion

        #region Variables
        volatile bool locked = false;
        DeviceSignature dev;
        int snmpPort;
        int timeout;
        volatile bool shouldStop = false;
        #endregion

        #region events
        public event AsyncIdentifyDeviceFinishedEventHandler IdentifySingleDeviceFinished;
        #endregion

        #region Constructor
        public IdentifyThread(int snmpPort, int timeout)
        {
            this.snmpPort = snmpPort; //save arguments
            this.timeout = timeout;
        }
        #endregion

        #region Methods
        /// <summary>
        /// give new informations for queries
        /// </summary>
        /// <param name="sig">device information</param>
        /// <returns>returns if it had success</returns>
        public bool SetDeviceSignature(DeviceSignature sig)
        {
            if (!locked) //if query is not running
            {
                dev = sig; //save information
            }
            return !locked; //return if successful
        }

        public void StopThread()
        {
            shouldStop = true;
        }

        /// <summary>
        /// Main Method for identify Device Thread
        /// </summary>
        public void IdentifyMethod()
        {
            while (!shouldStop) //endless loop
            {
                while (dev.mac == null) ; //if no device information is given, wait

                locked = true; //set thread is working
                DeviceType type;
                IPAddress workingIP = null;
                try
                {
                    IPAddress[] ips = IdentifyDevices.ConvertStringToIP(dev.ip);
                    bool success = false;
                    foreach (IPAddress ip in ips)
                    {
                        for (int i = 0; i < numberOfPings; i++) //try connection pings
                        {
                            Ping ping = new Ping(); //initialize ping object
                            if (ping.Send(ip, (timeout / timeoutDivisor)).Status == IPStatus.Success) //send ping with given timeout
                            {
                                success = true;
                                workingIP = ip;
                                break;  //break loop
                            }
                        }
                        if (success)
                            break;
                    }
                    type = IdentifyDevice(workingIP); //call the method to identify the device (synchron method)
                }
                catch
                {
                    type = DeviceType.unreachableDevice;
                }
                string[] stringIps = null;
                if(workingIP != null)
                    stringIps= new string[] { workingIP.ToString() };

                IdentifyDeviceEventArgs args = new IdentifyDeviceEventArgs(new DeviceSignature(stringIps, dev.mac, dev.port, type)); //initialize new event args object
                dev = new DeviceSignature(null, null, null); //set "no information given"
                locked = false; //set not longer locked
                if (this.IdentifySingleDeviceFinished != null) //if event is hooked up
                    this.IdentifySingleDeviceFinished(this, args); //throw event
            }
        }

        /// <summary>
        /// Scans a device, if it is reachable and which type of device it is
        /// </summary>
        /// <param name="ip">Ip Address</param>
        /// <returns></returns>
        private DeviceType IdentifyDevice(IPAddress ip)
        {
            
            if (ip != null) //if device had been reached
            {
                bool windowsPC;
                //bool printerPC;
                IdentifyDeviceCommands OIDS = new IdentifyDeviceCommands();                     //get the OID from the DataStorageClass
                List<Variable> oid = new List<Variable>();                                      //List of OID 
                oid.Add(new Variable(new ObjectIdentifier(OIDS["StandardRequest"])));               //Add the OID

                try                                                                             //trys to connect to the IP Address 
                {
                    var deviceInformation = Messenger.Get(VersionCode.V1,
                                     new IPEndPoint(ip, snmpPort),
                                     new OctetString("public"),
                                     oid,
                                     timeout);

                    windowsPC = deviceInformation[0].Data.ToString().ToLower().Contains("windows");        //filters the request 
                    //bool printer = deviceInformation[0].Data.ToString().ToLower().Contains("hp");         //filters the request 
                }
                catch
                {
                    return DeviceType.Device;   //if the device don't request it returns as a device (unkown)
                }
                if (windowsPC)
                    return DeviceType.PC;       //returns the device as a Windows PC

                //if (printer)                  //returns the device as a Printer 
                //    return DeviceType.Printer;

                return DeviceType.Printer;      //else return as a printer
            }
            else
            {
                return DeviceType.unreachableDevice; //if device could not be reached
            }
        }

        #endregion
    }

    public class RecheckThread
    {

        #region Constants
        const int timeoutDivisor = 5;
        const int numberOfPings = 2;
        #endregion

        #region Variables
        int timeout;
        IPCollection ips;
        volatile bool shouldStop = false;
        volatile bool isWorking = false;
        #endregion

        #region Events
        public event AsyncRecheckDeviceFinishedEventHandler SingleDeviceFinished;
        #endregion

        public RecheckThread(int timeout)
        {
            this.timeout = timeout;
        }

        #region Methods
        public void CheckMethod()
        {
            while (!shouldStop)
            {
                isWorking = true;
                bool success = false;
                IPAddress workingIP = null;
                IPAddress[] ips = IdentifyDevices.ConvertStringToIP(this.ips.IPs);

                for (int i = 0; i < numberOfPings; i++) //try connection pings
                {
                    Ping ping = new Ping(); //initialize ping object
                    if (ping.Send(this.ips.Dev.IP, (timeout / timeoutDivisor)).Status == IPStatus.Success) //send ping with given timeout
                    {
                        success = true;
                        break;  //break loop
                    }
                }
                if (!success)
                {
                    foreach (IPAddress ip in ips)
                    {
                        for (int i = 0; i < numberOfPings; i++) //try connection pings
                        {
                            Ping ping = new Ping(); //initialize ping object
                            if (ping.Send(ip, (timeout / timeoutDivisor)).Status == IPStatus.Success) //send ping with given timeout
                            {
                                success = true;
                                workingIP = ip;
                                break;  //break loop
                            }
                        }
                        if (success)
                            break;
                    }
                    this.ips.Dev.IP = workingIP;

                }
                isWorking = false;
                if (SingleDeviceFinished != null)
                    SingleDeviceFinished(this, EventArgs.Empty);
            }
        }

        public void SetIps(IPCollection ips)
        {
            if(!isWorking)
                this.ips = ips;
        }

        public void StopThread()
        {
            shouldStop = true;
        }
        #endregion

    }

    public class IdentifyDeviceEventArgs : EventArgs
    {
        public IdentifyDeviceEventArgs(DeviceSignature sig)
        {
            Signature = sig;
        }

        public DeviceSignature Signature{get; private set;} //add propterty Device Signature to event args
    }

    public class IdentifyDevicesEventArgs : EventArgs
    {
        public IdentifyDevicesEventArgs(DeviceSignature[] devs)
        {
            Devices = devs;
        }

        public DeviceSignature[] Devices { get; private set; } //add Propterty Array of device Signatures to event args
    }

    public struct DeviceSignature
    {
        public DeviceSignature(string[] ip, string mac, string port)
        {
            this.ip = ip; //save all given Variables
            this.mac = mac;
            this.port = port;
            this.type = DeviceType.Device;
        }

        public DeviceSignature(string[] ip, string mac, string port, DeviceType type)
        {
            this.ip = ip; //save all given Variables
            this.mac = mac;
            this.port = port;
            this.type = type;
        }

        #region Variables
        public readonly DeviceType type;
        public readonly string[] ip;
        public readonly string mac;
        public readonly string port;
        #endregion

        public override bool Equals(object obj)
        {
            if (obj == null) return false;
            DeviceSignature? other = obj as DeviceSignature?;
            if (other != null && other.Value.mac == this.mac) return true;
            return false;
        }

        public override int GetHashCode()
        {
            return base.GetHashCode();
        }
    }

    public struct IPCollection
    {
        public IPCollection(string[] ips, Device dev)
        {
            IPs = ips;
            Dev = dev;
        }
        public readonly string[] IPs;
        public readonly Device Dev;
    }
}
